#-----------------------------------------------------------------------------
#
#    minimal.py
#
#    Minimal OpenZWave example based off CPP version
#
#    Copyright (c) 2011 Evolui, LLC <evolui1@evolui.org>
#
#    SOFTWARE NOTICE AND LICENSE
#
#    This file is part of py-openzwave-extension.
#
#    py-openzwave-extension is free software: you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public License as
#    published by the Free Software Foundation, either version 3 of the License,
#    or (at your option) any later version.
#
#    py-openzwave-extension is distributed in the hope that it will be useful, 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public License
#    along with py-openzwave-extension.  If not, see
#    <http://www.gnu.org/licenses/>.
#
#-----------------------------------------------------------------------------
import os, time, glob, threading, copy, functools, pyopenzwave

g_criticalSection = threading.Lock()

def debug(*args):
    if True:
        print('**', *args)
        
command_classes = {
112 : "configuration",
113 : "alarm",
114 : "manufacturer_specific",
115 : "powerlevel",
117 : "protection",
118 : "lock",
119 : "node_naming",
128 : "battery",
129 : "clock",
130 : "hail",
132 : "wake_up",
133 : "association",
134 : "version",
135 : "indicator",
136 : "proprietary",
137 : "language",
142 : "multi_instance_association",
143 : "multi_cmd",
144 : "energy_production",
155 : "association_command_configuration",
156 : "sensor_alarm",
32 : "basic",
33 : "controller_replication",
34 : "application_status",
37 : "switch_binary",
38 : "switch_multilevel",
39 : "switch_all",
40 : "switch_toggle_binary",
41 : "switch_toggle_multilevel",
48 : "sensor_binary",
49 : "sensor_multilevel",
50 : "meter",
53 : "meter_pulse",
64 : "thermostat_mode",
66 : "thermostat_operating_state",
67 : "thermostat_setpoint",
68 : "thermostat_fan_mode",
69 : "thermostat_fan_state",
70 : "climate_control_schedule",
80 : "basic_window_covering",
96 : "multi_instance",
}

class ZWaveException(Exception):
    pass

class PyValueID():
    def __init__(self, homeid, nodeid, valueid):
        self.homeid = homeid
        self.nodeid = nodeid
        self.valueid = valueid
        
    def __get_min(self):
        return pyopenzwave.manager.GetValueMin(self.valueid)
    min = property(__get_min)

    def __get_max(self):
        return pyopenzwave.manager.GetValueMax(self.valueid)
    max = property(__get_max)

    def __str__(self):
        return str(self.type) + ":" + str(self.genre) + ":" + str(self.label) + ":" + str(self.units) + ":" + str(self.value)
    
    def __get_type(self):
        return self.valueid.GetType()
    type = property(__get_type)
    
    def __get_genre(self):
        return self.valueid.GetGenre()
    genre = property(__get_genre)
    
    def __get_label(self):
        return pyopenzwave.manager.GetValueLabel(self.valueid)
    def __set_label(self, value):
        pyopenzwave.manager.SetValueLabel(self.valueid, value)
        pyopenzwave.manager.WriteConfig(self.homeid)
    label = property(__get_label, __set_label)

    def __get_help(self):
        return pyopenzwave.manager.GetValueHelp(self.valueid)
    help = property(__get_help)
    
    def __get_classid(self):
        return self.valueid.GetCommandClassId()          
    classid = property(__get_classid)
    
    def __get_units(self):
        return pyopenzwave.manager.GetValueUnits(self.valueid)
    def __set_units(self, value):
        pyopenzwave.manager.SetValueUnits(self.valueid, value)
        pyopenzwave.manager.WriteConfig(self.homeid)
    units = property(__get_units, __set_units)
    
    def __get_readonly(self):
        return pyopenzwave.manager.IsValueReadOnly(self.valueid)
    readonly = property(__get_readonly)
    
    def __get_value(self):
        type = self.type.lower()

        if type == "bool":
            return pyopenzwave.manager.GetValueAsBool(self.valueid)
        elif type == "byte":
            return pyopenzwave.manager.GetValueAsByte(self.valueid)
        elif type == "decimal":
            return pyopenzwave.manager.GetValueAsFloat(self.valueid)
        elif type == "int":
            return pyopenzwave.manager.GetValueAsInt(self.valueid)
        elif type == "short":
            return pyopenzwave.manager.GetValueAsShort(self.valueid)
        elif type == "string":
            return pyopenzwave.manager.GetValueAsString(self.valueid)
        elif type == "list":
            return controllers.base.ListAttribute.jsonify(pyopenzwave.manager.GetValueListItems(self.valueid), selected = pyopenzwave.manager.GetValueListSelection(self.valueid))
    def __set_value(self, value):
        if pyopenzwave.manager.IsValueReadOnly(self.valueid):
            return
        
        typ = self.type.lower()
        
        if typ == "list":
            if type(value) in (list, tuple) and len(value) == 1:
                value = value[0]
            if type(value) is not str:
                return

        #
        # typ has to be specific python value in order for SetValue to
        # recognize which internal "set" function to call
        #
        if typ == "bool":
            value = bool(value)
        elif typ in ("byte", "int", "short"):
            value = int(value)
        elif typ == "decimal":
            value = float(value)
        elif typ == "string":
            value = str(value)

        #print("VALUE ADDED", self.valueid, value, typ)
        try:
            pyopenzwave.manager.SetValue(self.valueid, value)
        except pyopenzwave.OpenZWaveException as e:
            print('***Cant set value for', self.label, type(value), value)
        
    value = property(__get_value, __set_value)
    
    def convert(self, other):
        if isinstance(other, pyopenzwave.ValueID):
            return other
        else:
            return other.valueid
        
    def __eq__(self, other):
        return self.valueid == self.convert(other)

    def __lt__(self, other):
        return self.valueid < self.convert(other)

    def __ne__(self, other):
        return self.valueid != self.convert(other)

    def __gt__(self, other):
        return self.valueid > self.convert(other)

    def __le__(self, other):
        return self.valueid <= self.convert(other)

    def __ge__(self, other):
        return self.valueid >= self.convert(other)

    def __get_polling(self):
        return pyopenzwave.manager.isPolled(self.valueid)
    def __set_polling(self, value):
        if value:
            pyopenzwave.manager.EnablePoll(self.valueid)
        else:
            pyopenzwave.manager.DisablePoll(self.valueid)
    polling = property(__get_polling, __set_polling)

class PyNode():
    def __init__(self, homeid, nodeid, polling = False):
        self.homeid = homeid
        self.nodeid = nodeid
        self._polling = polling
        # stored here so a label for a node can be identified when it's been
        # removed from the network
        self._cached_label = str(nodeid)
        self._cached_basic = None

    def __get_basic(self):
        if self._cached_basic is None:
            self._cached_basic = pyopenzwave.manager.GetNodeBasic(self.homeid, self.nodeid)
        return self._cached_basic
    basic = property(__get_basic)
    
    def __get_label(self):
        label = pyopenzwave.manager.GetNodeName(self.homeid, self.nodeid)
        if label:
            self._cached_label = label
        else:
            label = self._cached_label
        return label
    def __set_label(self, value):
        pyopenzwave.manager.SetNodeName(self.homeid, self.nodeid, value)
        pyopenzwave.manager.WriteConfig(self.homeid)
        self._cached_label = value
    label = property(__get_label, __set_label)
        
    def __get_location(self):
        return pyopenzwave.manager.GetNodeLocation(self.homeid, self.nodeid)
    def __set_location(self, value):
        pyopenzwave.manager.SetNodeLocation(self.homeid, self.nodeid, value)
        pyopenzwave.manager.WriteConfig(self.homeid)
    location = property(__get_location, __set_location)
    
    def __get_listening(self):
        return pyopenzwave.manager.IsNodeListeningDevice(self.homeid, self.nodeid)
    listening = property(__get_listening)
    
    def __get_routing(self):
        return pyopenzwave.manager.IsNodeRoutingDevice(self.homeid, self.nodeid)
    routing = property(__get_routing)

    def _get_manufacturer(self):
        return pyopenzwave.manager.GetNodeManufacturerName(self.homeid, self.nodeid)
    def _set_manufacturer(self, value):
        pyopenzwave.manager.SetNodeManufacturerName(self.homeid, self.nodeid, value)
        pyopenzwave.manager.WriteConfig(self.homeid)
    manufacturer = property(_get_manufacturer, _set_manufacturer)

    def _get_productname(self):
        return pyopenzwave.manager.GetNodeProductName(self.homeid, self.nodeid)
    def _set_productname(self, value):
        pyopenzwave.manager.SetNodeProductName(self.homeid, self.nodeid, value)
        pyopenzwave.manager.WriteConfig(self.homeid)
    productname = property(_get_productname, _set_productname)

    def _get_version(self):
        return pyopenzwave.manager.GetNodeVersion(self.homeid, self.nodeid)
    version = property(_get_version)

    def refresh(self):
        pyopenzwave.manager.RequestNodeState(self.homeid, self.nodeid)
        pyopenzwave.manager.RefreshNodeInfo(self.homeid, self.nodeid)
            
    def is_main_controller(self):
        return pyopenzwave.manager.GetControllerNodeId(self.homeid) == self.nodeid 

class ZWave:
    def __init__(self, port = None, highpower = True):
        self.port = port
        self.nodemap = {} # homeid to nodeid to node
        self.valuemap = {} # homeid to nodeid to value
        self.homemap = {} # homeid to home map
        self.highpower = highpower
        
    def refresh_nodes(self, homeid):
        for nodeid in self.nodemap[homeid]:
            node = self.nodemap[homeid][nodeid]
            debug("refreshing node", homeid, nodeid)
            self.setactive(autotrigger = True)
            node.refresh()
            # some nodes may be asleep, so don't wait too long for them
            self.wait(autotriggertimeout = 1.0)
                    
    def on_controller_command(self, state, message = "", command = None):
        g_criticalSection.acquire()
        if state == pyopenzwave.Driver.ControllerState_Normal:
            pass
        elif state == pyopenzwave.Driver.ControllerState_Waiting:
            pass
        elif state == pyopenzwave.Driver.ControllerState_InProgress:
            pass
        elif state == pyopenzwave.Driver.ControllerState_Completed:
            pass
        elif state == pyopenzwave.Driver.ControllerState_Failed:
            pass
        print("processing command %s with state of %s, message: " % (str(command), str(state), str(message)))
        g_criticalSection.release()
        
    def issue_controller_command(self, homeid, nodeid, command, message):
        args = [] # positional args to pass on to on_controller_command
        kwargs = {message : "custom message to pass to on_controller_command", command : command}
        
        if command in (pyopenzwave.Driver.ControllerCommand_AddDevice, pyopenzwave.Driver.ControllerCommand_AddController):
            self.zwave.setactive()
            
        success = pyopenzwave.manager.BeginControllerCommand(homeid, command, self.highpower, nodeid, self.on_controller_command, *args, **kwargs)
    
    def lookup(self, homeid, nodeid):
        if homeid in self.nodemap and nodeid in self.nodemap[homeid]:
            return self.nodemap[homeid][nodeid]
        else:
            return PyNode(homeid, nodeid)

    def on_notification(self, _notification):
        debug("got A notification ", _notification.GetType())

        try:
            g_criticalSection.acquire()
            
            nodeid = _notification.GetNodeId()
            homeid = _notification.GetHomeId()
            valueid = _notification.GetValueID()
            pyvalueid = PyValueID(homeid, nodeid, valueid)
            
            pynode = self.lookup(homeid, nodeid)
            
            iscontroller = pynode is not None and pynode.basic in (1,2)
            
            nodelabel = pyopenzwave.manager.GetNodeName(homeid, nodeid) or str(nodeid)
            
            # ISSUE: when zwave is first initialized, or when a controller is
            # first added, the node.basic seems to return an incorrect value.
                
            home = self.homemap.get(homeid, None)
            
            if _notification.GetType() == pyopenzwave.Notification.Type_DriverReady:
                debug("Device found and ready!")
                if homeid not in self.nodemap:
                    self.nodemap[homeid] = {}
                
            elif _notification.GetType() == pyopenzwave.Notification.Type_NodeAdded:
                debug("Node added with id of", nodeid)
                if homeid not in self.nodemap:
                    self.nodemap[homeid] = {}
    
                self.nodemap[homeid][nodeid] = pynode
                
                pyopenzwave.manager.WriteConfig(homeid)
            elif _notification.GetType() == pyopenzwave.Notification.Type_NodeQueriesComplete:
                # TBD
                pass
            elif _notification.GetType() == pyopenzwave.Notification.Type_NodeRemoved:
                try:
                    node = self.nodemap[homeid].pop(nodeid)
                except KeyError as e:
                    # if the user tried removing a node that wasn't added in the
                    # first place
                    pass
                
                pyopenzwave.manager.WriteConfig(homeid)
    
            elif _notification.GetType() == pyopenzwave.Notification.Type_ValueAdded:
                
                # DISPATCH SPECIFIC
                if pyvalueid.genre != "PUT_FILTER_HERE":
                    self.valuemap[homeid][nodeid][valueid] = pyvalueid

                pyopenzwave.manager.WriteConfig(homeid)
    
            elif _notification.GetType() == pyopenzwave.Notification.Type_ValueRemoved:
                
                debug("remove value")
                if homeid in self.valuemap and nodeid in self.valuemap[homeid]:
                    try:
                        del self.valuemap[homeid][nodeid][valueid]
                    except KeyError as e:
                        pass
                
                pyopenzwave.manager.WriteConfig(homeid)
    
            elif _notification.GetType() == pyopenzwave.Notification.Type_ValueChanged:
                if pyvalueid.genre != "PUT_FILTER_HERE":
                    # TBD - do something with pyvalueid
                    pass
                        
            elif _notification.GetType() == pyopenzwave.Notification.Type_Group:
    
                debug("group association updated for ", pynode)
                
            elif _notification.GetType() == pyopenzwave.Notification.Type_NodeEvent:
                debug("requesting node state for ", nodeid)
                debug("value ", _notification.GetEvent())

                #pyopenzwave.manager.RequestNodeState(homeid, nodeid)
                #pyvalueid.value = _notification.GetEvent()
            
            elif _notification.GetType() == pyopenzwave.Notification.Type_PollingDisabled:
                pass
            elif _notification.GetType() == pyopenzwave.Notification.Type_PollingEnabled:
                pass
            elif _notification.GetType() == pyopenzwave.Notification.Type_NodeProtocolInfo:
                pass
                # TBDinitialize
            elif _notification.GetType() == pyopenzwave.Notification.Type_NodeNaming:
                # TODO: NOT getting this notification from zwave.
                pass
            elif _notification.GetType() == pyopenzwave.Notification.Type_DriverReset:
                pass
                # TBD
        
        finally:
            g_criticalSection.release()

    def tryport(self, port):
        """
        there is a bug in openzwave that causes a hard crash when attempting to
        remove a driver.  therefore, the driver is just left inside the library
        if it's found to be invalid.
        """
        pyopenzwave.manager.AddDriver(port, pyopenzwave.Driver.ControllerInterface_Serial)
        
        debug("Trying port %s for zwave controller" % port)
        for i in range(5):
            time.sleep(1)
            if self.nodemap:
                debug("Successfully found zwave controller at %s !" % port)
                return True
            
        debug("Failed to find zwave controller at %s" % port)
        return False
             
    def connect(self):
        pyopenzwave.options.Create("", "", "")
        pyopenzwave.options.Lock()
        pyopenzwave.manager.Create()
        #pyopenzwave.manager.SetPollInterval(10)
        
        pyopenzwave.manager.AddWatcher(self.on_notification)
        
        success = False
            
        if self.port is not None:
            # manual specification
            success = self.tryport(self.port)
        else:
            # auto specification - search for correct port
            if os.name == "posix":
                ports = glob.glob("/dev/ttyUSB*")
            else:
                ports = ["\\\\.\\COM6", "\\\\.\\COM5", "\\\\.\\COM3", "\\\\.\\COM4", "\\\\.\\COM2", "\\\\.\\COM1"]

            for port in ports:
                success = self.tryport(port)
                if success:
                    self.port = port
                    break
        
        return success

if __name__ == "__main__":
    zwave = ZWave()
    zwave.connect()
    
    #g_criticalSection.acquire()
    #g_criticalSection.release()
    #print("main", threading.current_thread())
    while(not zwave.nodemap):
        time.sleep(0.01)
    time.sleep(0.01)

    # assumes only one homeid
    homeid = list(zwave.nodemap.keys())[0]
    
    pyopenzwave.manager.WriteConfig(homeid)
    while True:
        time.sleep(0.01)
        g_criticalSection.acquire()
        # Do stuff
        g_criticalSection.release()
    
    g_criticalSection.release()
    sys.exit(0)
    